package org.tamal.classinfo;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * This class iterates over the class path.
 * @author Tamal Kanti Nath
 */
public final class ClassIterator
    implements Iterable<ClassInfo>, Iterator<ClassInfo> {

    private static final String EXT = ".class";
    private final Set<String> path = new TreeSet<>();
    private Queue<File> dirs;
    private Queue<File> jars;
    private JarFile jarFile;
    private Enumeration<JarEntry> jarEntries;
    private ClassInfo info;

    /**
     * Creates a new ClassIterator instance with boot, extension and system
     * class path.
     */
    public ClassIterator() {
        String p = System.getProperty("sun.boot.class.path");
        path.addAll(Arrays.asList(p.split(File.pathSeparator)));
        p = System.getProperty("java.ext.dirs");
        for (String dir: p.split(File.pathSeparator)) {
            String[] array = new File(dir).list();
            if (array != null) {
                path.addAll(Arrays.asList(array));
            }
        }
        p = System.getProperty("java.class.path");
        path.addAll(Arrays.asList(p.split(File.pathSeparator)));
    }

    /**
     * Checks if the identifier is valid or not.
     * @param identifier the identifier name
     * @return true if valid, false otherwise
     */
    public static boolean isValidIdentifier(String identifier) {
        final String[] reserved = {"abstract", "assert", "boolean", "break",
            "byte", "case", "catch", "char", "class", "const", "continue",
            "default", "do", "double", "else", "enum", "extends", "false",
            "final", "finally", "float", "for", "goto", "if", "implements",
            "import", "instanceof", "int", "interface", "long", "native", "new",
            "null", "package", "private", "protected", "public", "return",
            "short", "static", "strictfp", "super", "switch", "synchronized",
            "this", "throw", "throws", "transient", "true", "try", "void",
            "volatile", "while"};
        boolean valid = true;
        if (!Character.isJavaIdentifierStart(identifier.charAt(0))) {
            valid = false;
        }
        if (valid) {
            for (char ch : identifier.toCharArray()) {
                if (!Character.isJavaIdentifierPart(ch)) {
                    valid = false;
                    break;
                }
            }
        }
        if (valid) {
            for (String word : reserved) {
                if (word.equals(identifier)) {
                    valid = false;
                    break;
                }
            }
        }
        return valid;
    }

    /**
     * Adds custom class path or jar file.
     * @param cp the external class path or jar file
     * @return false if class path is invalid or already added, true otherwise
     */
    public boolean addClassPath(String cp) {
        if (new File(cp).isDirectory()) {
            return path.add(cp);
        }
        try (JarFile jar = new JarFile(cp)) {
            return path.add(cp);
        } catch (IOException e) {
            return false;
        }
    }

    @Override
    public boolean hasNext() {
        if (info == null) {
            try {
                info = nextDirClass();
            } catch (NoSuchElementException e) {
                info = null;
            }
        }
        if (info == null) {
            try {
                info = nextJarClass();
            } catch (NoSuchElementException e) {
                info = null;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        if (info == null) {
            return false;
        }
        return true;
    }

    @Override
    public ClassInfo next() {
        hasNext();
        ClassInfo temp = info;
        info = null;
        return temp;
    }

    private ClassInfo nextDirClass() {
        while (true) {
            File file = dirs.remove();
            while (file.isDirectory()) {
                if (isValidIdentifier(file.getName())) {
                    dirs.addAll(Arrays.asList(file.listFiles()));
                }
                file = dirs.remove();
            }
            String name = file.getName();
            if (name.endsWith(EXT)) {
                name = name.substring(0, name.length() - 6);
                if (isValidIdentifier(name)) {
                    return new ClassInfo(file);
                }
            }
        }
    }

    private ClassInfo nextJarClass() throws IOException {
        while (true) {
            while (!jarEntries.hasMoreElements()) {
                jarFile.close();
                jarFile = new JarFile(jars.remove());
                jarEntries = jarFile.entries();
            }
            JarEntry entry = jarEntries.nextElement();
            if (isValidJarClass(entry)) {
                String url = jarFile.getName().replace(File.separatorChar, '/');
                url = "jar:file:" + url + "!/" + entry.getName();
                return new ClassInfo(new URL(url).openStream());
            }
        }
    }

    private static boolean isValidJarClass(JarEntry entry) {
        String name = entry.getName();
        if (!entry.isDirectory() && name.endsWith(EXT)) {
            name = name.substring(0, name.length() - 6);
            boolean valid = true;
            for (String identifier : name.split("/")) {
                if (!isValidIdentifier(identifier)) {
                    valid = false;
                    break;
                }
            }
            return valid;
        }
        return false;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Iterator<ClassInfo> iterator() {
        dirs = new LinkedList<>();
        jars = new LinkedList<>();
        for (String pathname : path) {
            File file = new File(pathname);
            if (file.isDirectory()) {
                dirs.add(file);
            }
            if (file.isFile()) {
                jars.add(file.getAbsoluteFile());
            }
        }
        try {
            jarFile = new JarFile(jars.remove());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        jarEntries = jarFile.entries();
        hasNext();
        return this;
    }
}
